/*
 * This file contains drawing utility classes and function.
 */

// ======================== POSITION ========================
// 
// A simpe class that represents a position on the canvas
function Position(x, y) {
    this.x = x;
    this.y = y;
}
Position.prototype.sum = function(other) {
    return new Position(this.x + other.x, this.y + other.y);
};
Position.prototype.subtract = function(other) {
    return new Position(this.x - other.x, this.y - other.y);
};
Position.prototype.dot = function(other) {
    return this.x * other.x + this.y * other.y;
};
Position.prototype.scale = function(s) {
    return new Position(this.x * s, this.y * s);
};
Position.prototype.length = function() {
    return Math.sqrt(this.dot(this));
};
Position.prototype.distance = function(other) {
    return this.subtract(other).length();
};
Position.prototype.normalize = function() {
    var l = this.length();
    this.x /= l;
    this.y /= l;
    return this;
};
Position.prototype.interpolate = function(other, t) {
    return new Position((1 - t) * this.x + t * other.x, (1 - t) * this.y + t * other.y);
};
Position.prototype.toString = function() {
    return "[Position x=" + this.x + ", y=" + this.y + "]";
};

function StringToPosition(posStr)
{
    posxy = posStr.split(",");
    return new Position(Number(posxy[0]), Number(posxy[1]));
}

// ======================== DIMENSION ========================
// 
// A simpe class that represents a dimension (width and height)
function Dimension(x, y) {
    this.x = x;
    this.y = y;
}
Position.prototype.scale = function(s) {
    return new Dimension(this.x * s, this.y * s);
};
Dimension.prototype.toString = function() {
    return "[Dimension x=" + this.x + ", y=" + this.y + "]";
};

// ======================== SINGLE LINE RENDERING FUNCTIONS ========================
// 
// These functions draw lines (and shapes consisting of line) with single pixel width.
// The reason they are used is that a canvas uses antialiasing for 1px wide 
// horizontal/vertical lines by default.

// Draw 1px line horizontally with length w starting from (sx,sy)
function drawHorizontalLine1px(sx, sy, w, color) {
    canvasCtx.fillStyle = color;
    canvasCtx.fillRect(sx, sy, w, 1);
}
// Draw 1px line vertically with length h starting from (sx,sy)
function drawVerticalLine1px(sx, sy, h, color) {
    canvasCtx.fillStyle = color;
    canvasCtx.fillRect(sx, sy, 1, h);
}
// Draw a rectangle with top-left corner at (sx,sy) and dimensions (w,h) 
// using 1px lines.
function drawBox1px(sx, sy, w, h, color) {
    canvasCtx.fillStyle = color;
    drawHorizontalLine1px(sx, sy, w);
    drawHorizontalLine1px(sx, sy + h - 1, w);
    drawVerticalLine1px(sx, sy, h);
    drawVerticalLine1px(sx + w - 1, sy, h);
}

function drawLine(pos0, pos1, color) {
    color = (color !== undefined) ? color : "#000000";
    canvasCtx.lineWidth = 1;
    canvasCtx.strokeStyle = color;
    canvasCtx.beginPath();
    canvasCtx.moveTo(pos0.x, pos0.y);
    canvasCtx.lineTo(pos1.x, pos1.y);
    canvasCtx.stroke();
    canvasCtx.closePath();
}

function drawDashedLine(pos0, pos1, color, dashLen, spaceLen) {
    color = (color !== undefined) ? color : "#000000";
    dashLen = (dashLen !== undefined) ? dashLen : 5;
    spaceLen = (spaceLen !== undefined) ? spaceLen : dashLen;

    // TODO implement spaceLen too!!

    var x0 = pos0.x;
    var y0 = pos0.y;
    var x1 = pos1.x;
    var y1 = pos1.y;

    canvasCtx.lineWidth = 1;
    canvasCtx.strokeStyle = color;

    canvasCtx.beginPath();
    canvasCtx.moveTo(x0, y0);

    var dx = x1 - x0;
    var dy = y1 - y0;
    var dashes = Math.floor(Math.sqrt(dx * dx + dy * dy) / dashLen);
    var dashX = dx / dashes;
    var dashY = dy / dashes;

    var q = 0;
    while (q++ < dashes) {
        x0 += dashX;
        y0 += dashY;
        canvasCtx[q % 2 === 0 ? 'moveTo' : 'lineTo'](x0, y0);
    }
    canvasCtx[q % 2 === 0 ? 'moveTo' : 'lineTo'](x1, y1);

    canvasCtx.stroke();
    canvasCtx.closePath();
}

function drawPolygon(verticesArr, fillColor, lineColor) {
    fillColor = (fillColor !== undefined) ? fillColor : "#000000";
    lineColor = (lineColor !== undefined) ? lineColor : fillColor;
    verticesArr = (verticesArr !== undefined) ? verticesArr : [];

    canvasCtx.lineWidth = 1;
    canvasCtx.lineJoin = "round";
    canvasCtx.strokeStyle = lineColor;
    canvasCtx.fillStyle = fillColor;

    canvasCtx.beginPath();
    for (var i = 0; i < verticesArr.length; i++) {
        var p = verticesArr[i];
        if (i > 0) {
            canvasCtx.lineTo(p.x, p.y);
        } else {
            canvasCtx.moveTo(p.x, p.y);
        }
    }
    canvasCtx.closePath();
    canvasCtx.fill();
    canvasCtx.stroke();
}

function drawLineStrip(verticesArr, lineColor) {
    lineColor = (lineColor !== undefined) ? lineColor : lineColor;
    verticesArr = (verticesArr !== undefined) ? verticesArr : [];

    canvasCtx.lineWidth = 1;
    canvasCtx.lineJoin = "round";
    canvasCtx.strokeStyle = lineColor;

    canvasCtx.beginPath();
    for (var i = 0; i < verticesArr.length; i++) {
        var p = verticesArr[i];
        if (i > 0) {
            canvasCtx.lineTo(p.x, p.y);
        } else {
            canvasCtx.moveTo(p.x, p.y);
        }
    }
    canvasCtx.stroke();
}

// ======================== RELATIONSHIP RENDERING FUNCTIONS ========================
// 

// get closest points for two boxes. Inputs 'cur0' and 'cur1' existing closest 
// estimates can be supplied.
function closestPointsBoxes(pos0, dim0, pos1, dim1, prevClosest0, prevClosest1) {
    prevClosest0 = (prevClosest0 !== undefined) ? prevClosest0 : new Position(0, 0);
    prevClosest1 = (prevClosest1 !== undefined) ? prevClosest1 : new Position(0, 0);

    // determine closest features (edge-edge, corner-corner)
    var b0 = pos0.x > pos1.x + dim1.x;
    var b1 = pos1.x > pos0.x + dim0.x;
    var b3 = pos0.y > pos1.y + dim1.y;
    var b2 = pos1.y > pos0.y + dim0.y;

    var code0 = (b0 ? 2 : 0) + (b1 ? 1 : 0);
    var code1 = (b2 ? 2 : 0) + (b3 ? 1 : 0);
    var code = 3 * code1 + code0;
    var p0 = new Position();
    var p1 = new Position();

    switch (code) {
        case 0: // intersection
            p0 = (prevClosest0 !== undefined) ? prevClosest0 : pos0;
            p1 = (prevClosest1 !== undefined) ? prevClosest1 : pos1;
            break;

        case 1: // edge-edge: axis[X]: max0, min1
            p0.x = pos0.x + dim0.x;
            p0.y = Clamp(prevClosest0.y, Math.max(pos0.y, pos1.y), Math.min(pos0.y + dim0.y, pos1.y + dim1.y));
            p1.x = pos1.x;
            p1.y = Clamp(prevClosest1.y, Math.max(pos0.y, pos1.y), Math.min(pos0.y + dim0.y, pos1.y + dim1.y));
            break;

        case 2: // edge-edge: axis[X]: min0, max1
            p0.x = pos0.x;
            p0.y = Clamp(prevClosest0.y, Math.max(pos0.y, pos1.y), Math.min(pos0.y + dim0.y, pos1.y + dim1.y));
            p1.x = pos1.x + dim1.x;
            p1.y = Clamp(prevClosest1.y, Math.max(pos0.y, pos1.y), Math.min(pos0.y + dim0.y, pos1.y + dim1.y));
            break;

        case 3: // edge-edge: axis[Y]: max0, min1
            p0.x = Clamp(prevClosest0.x, Math.max(pos0.x, pos1.x), Math.min(pos0.x + dim0.x, pos1.x + dim1.x));
            p0.y = pos0.y;
            p1.x = Clamp(prevClosest1.x, Math.max(pos0.x, pos1.x), Math.min(pos0.x + dim0.x, pos1.x + dim1.x));
            p1.y = pos1.y + dim1.y;
            break;

        case 4: // corner-corner: topright0,bottomleft1
            p0.x = pos0.x + dim0.x;
            p0.y = pos0.y;
            p1.x = pos1.x;
            p1.y = pos1.y + dim1.y;
            break;

        case 5: // corner-corner: topleft0,bottomright1
            p0.x = pos0.x;
            p0.y = pos0.y;
            p1.x = pos1.x + dim1.x;
            p1.y = pos1.y + dim1.y;
            break;

        case 6: // edge-edge: axis[Y]: min0, max1
            p0.x = Clamp(prevClosest0.x, Math.max(pos0.x, pos1.x), Math.min(pos0.x + dim0.x, pos1.x + dim1.x));
            p0.y = pos0.y + dim0.y;
            p1.x = Clamp(prevClosest1.x, Math.max(pos0.x, pos1.x), Math.min(pos0.x + dim0.x, pos1.x + dim1.x));
            p1.y = pos1.y;
            break;

        case 7: // corner-corner: bottomright0,topleft1
            p0.x = pos0.x + dim0.x;
            p0.y = pos0.y + dim0.y;
            p1.x = pos1.x;
            p1.y = pos1.y;
            break;

        case 8: // corner-corner: bottomleft0,topright1
            p0.x = pos0.x;
            p0.y = pos0.y + dim0.y;
            p1.x = pos1.x + dim1.x;
            p1.y = pos1.y;
            break;

    }

    return {c0: p0, c1: p1};

}

function drawDiamond(pos0, pos1, fillColor, lineColor, scale) {

    var dir = pos1.subtract(pos0);
    dir.normalize();
    var n = new Position(-dir.y, dir.x);

    var quadHead = pos1;
    var quadCenter = pos1.subtract(dir.scale(scale));
    var quadLeft = quadCenter.sum(n.scale(0.66 * scale));
    var quadRight = quadCenter.sum(n.scale(-0.66 * scale));
    var quadEnd = quadCenter.subtract(dir.scale(scale));

    drawPolygon([quadHead, quadLeft, quadEnd, quadRight], fillColor, lineColor);

    return quadEnd;
}

function drawArrowHead(pos0, pos1, lineColor, scale) {

    var dir = pos1.subtract(pos0);
    dir.normalize();
    var n = new Position(-dir.y, dir.x);

    var arrowHead = pos1;
    var arrowCenter = pos1.subtract(dir.scale(scale));
    var arrowLeft = arrowCenter.sum(n.scale(0.66 * scale));
    var arrowRight = arrowCenter.sum(n.scale(-0.66 * scale));

    drawLineStrip([arrowRight, arrowHead, arrowLeft], lineColor);

    return arrowHead;
}

function drawFilledArrowHead(pos0, pos1, fillColor, lineColor, scale) {

    var dir = pos1.subtract(pos0);
    dir.normalize();
    var n = new Position(-dir.y, dir.x);

    var arrowHead = pos1;
    var arrowCenter = pos1.subtract(dir.scale(scale));
    var arrowLeft = arrowCenter.sum(n.scale(0.66 * scale));
    var arrowRight = arrowCenter.sum(n.scale(-0.66 * scale));

    drawPolygon([arrowRight, arrowHead, arrowLeft], fillColor, lineColor);

    return arrowCenter;
}

function drawDependency(pos0, pos1, color, arrowScale) {
    color = (color !== undefined) ? color : "#000000";
    arrowScale = (arrowScale !== undefined) ? arrowScale : 15;

    var p = drawArrowHead(pos0, pos1, color, arrowScale);
    drawDashedLine(pos0, p, color);
}

function drawGeneralization(pos0, pos1, color, arrowScale) {
    color = (color !== undefined) ? color : "#000000";
    arrowScale = (arrowScale !== undefined) ? arrowScale : 15;

    var p = drawFilledArrowHead(pos0, pos1, "#ffffff", color, arrowScale);
    drawLine(pos0, p, color);
}

function drawRealization(pos0, pos1, color, arrowScale) {
    color = (color !== undefined) ? color : "#000000";
    arrowScale = (arrowScale !== undefined) ? arrowScale : 15;

    var p = drawFilledArrowHead(pos0, pos1, "#ffffff", color, arrowScale);
    drawDashedLine(pos0, p, color);
}

function drawAssociation(pos0, pos1, navig0, navig1, color, arrowScale) {
    color = (color !== undefined) ? color : "#000000";
    arrowScale = (arrowScale !== undefined) ? arrowScale : 15;

    if(navig1 && !navig0) {
        p = drawArrowHead(pos0, pos1, color, arrowScale);
        drawLine(pos0, p, color);
        return;
    }
    if(navig0 && !navig1) {
        var p = drawArrowHead(pos1, pos0, color, arrowScale);
        drawLine(pos1, p, color);
        return;
    }

    drawLine(pos0, pos1, color);
}

function drawAggregation(pos0, pos1, navig0, navig1, color, marbleScale) {
    navig0 = (navig0 !== undefined) ? navig0 : true;
    navig1 = (navig1 !== undefined) ? navig1 : true;
    color = (color !== undefined) ? color : "#000000";
    marbleScale = (marbleScale !== undefined) ? marbleScale : 15;

    var p = drawDiamond(pos1, pos0, "#ffffff", color, marbleScale);
    drawAssociation(p, pos1, navig0, navig1, color, marbleScale);
}

function drawComposition(pos0, pos1, navig0, navig1, color, marbleScale) {
    navig0 = (navig0 !== undefined) ? navig0 : true;
    navig1 = (navig1 !== undefined) ? navig1 : true;
    color = (color !== undefined) ? color : "#000000";
    marbleScale = (marbleScale !== undefined) ? marbleScale : 15;

    var p = drawDiamond(pos1, pos0, color, color, marbleScale);
    drawAssociation(p, pos1, navig0, navig1, color, marbleScale);
}

function drawRelationshipStereotype(pos0, pos1, stereotype, textFormat) {

    var dir = pos1.subtract(pos0);
    dir.normalize();
    var n = new Position(-dir.y, dir.x);

    var center = pos0.interpolate(pos1, 0.5);
    
    canvasCtx.textBaseline = "middle";
    canvasCtx.fillStyle = textFormat.fontColor;
    canvasCtx.font = textFormat.getFontStyle();
    
    var w = canvasCtx.measureText(stereotype).width;
    
    var pos = new Position(center.x,center.y);
    var f = 8;
    
    if(dir.y === 0) {
        // horizontal
        canvasCtx.textAlign = "center";
        pos.x = center.x + n.x*f;
        pos.y = center.y + n.y*f - ((dir.x > 0)?(textFormat.fontSize * 1.5):0);
        canvasCtx.fillText(stereotype, pos.x, pos.y);
    } else {
        canvasCtx.textAlign = "left";
        pos.x = center.x + n.x*f - (((dir.y < 0) === (dir.x > 0))?w:0);
        pos.y = center.y + n.y*f - ((dir.x > 0)?(textFormat.fontSize * 1.5):0);
        canvasCtx.fillText(stereotype, pos.x, pos.y);
    }
}